Javascript library functions
A ready to-go example is in this repository, Clone it to <AppData>/wljs-notebook/wljs_packages
git clone https://github.com/JerryI/wljs-plugin-example-1
and restart WLJS Notebook
Here is the simples example on what you can do with extensions. Why not to add ApexCharts? They looks beautiful and already animated. What we need
- Kernel package, which implements
ApexCharts[]
symbols - Javascript bundle, which should include ApexCharts and bridge it with Wolfram Kernel using WLJS Functions
Summary what will be done
- package for evaluation kernel, which adds a new symbol
ApexCharts
- Javascript module, which renders the content of
ApexCharts
expression
Preparations
Use wljs-plugin-template template and create a new repository. Then clone new repository to <AppData>/wljs-notebook/wljs_packages
folder. For example
git clone https://github.com/JerryI/wljs-plugin-example-1
Then edit the content of package.json
{
"name": "wljs-plugin-example-1",
"version": "0.0.1",
"scripts": {
"build": "node --max-old-space-size=8192 ./node_modules/.bin/rollup --config rollup.config.mjs"
},
"description": "An example plugin for WLJS Notebook. Library functions",
"wljs-meta": {
"kernel": [
"src/Kernel.wl"
],
"js": "dist/kernel.js",
"minjs": "dist/kernel.min.js",
"priority": 5000,
"category": "Notebook Extensions"
},
"repository": {
"type": "git",
"url": "https://github.com/JerryI/wljs-plugin-example-1"
},
"dependencies": {
"@rollup/plugin-commonjs": "^25.0.4",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.2.1",
"@rollup/plugin-terser": "^0.4.4",
"rollup": "^3.21.6"
}
}
By the default a template implies, that we will use rollup.js for bundling. Run in the root directory of this package
npm i
And then install apexcharts
npm i apexcharts
Kernel package
Create a new file in src/Kernel.wl
, which is going to be our package for evaluation kernel. Looking at ApexCharts API, it is easy to imagine the way how it can be used
ApexCharts[<|
"series" -> {44, 55, 67, 83},
"labels" -> {"Apples", "Oranges", "Bananas", "Berries"},
"chart" -> <|
"height" -> 350,
"type" -> "radialBar"
|>
|>]
or any other way, one can pre-transform the data and use intermediate symbols. Following the simplest path, we write to our Kernel file
BeginPackage["CoffeeLiqueur`Extensions`ApexCharts`"]
(* Public context *)
ApexCharts::usage = "ApexCharts[a_Association] constructor"
Begin["`Private`"]
End[]
EndPackage[]
We can put a few checks to ensure that input is an association
nonAssocHeadQ[_] = True
nonAssocHeadQ[_Association] = False
ApexCharts::notassoc = "Input is not an association"
ApexCharts[_?nonAssocHeadQ ] := (Message[ApexCharts::notassoc]; $Failed)
Now we can think about ApexCharts
as if it was a new entity, and the interpretation of this entity will be our graphs. To draw actual graphs instead of a symbolic representation we need to define an output form. For that ViewBox comes in hand, which is provided in CoffeeLiqueur`Extensions`Boxes`
context
BeginPackage["CoffeeLiqueur`Extensions`ApexCharts`", {
"CoffeeLiqueur`Extensions`Boxes`"
}]
(* Public context *)
...
(* Output forms *)
ApexCharts /: MakeBoxes[a: ApexCharts[_Association], StandardForm ] := With[{},
ViewBox[a, a]
]
First a
will be an underlying expression (behind the graph), while second a
is going to be rendered instead of it. However, if you try to evaluate this with defined output form, you get a similar error
ViewBox tries to execute the expression provided as the second argument in the browser, but ApexCharts
does not exist there.
Javascript Library
Now we need to implement ApexCharts
on the frontend. The idea is simple: take the provided data and using Apex API render a graph on the given DOM element
let ApexCharts;
core.ApexCharts = async (args, env) => {
if (!ApexCharts) ApexCharts = (await import('apexcharts')).default; //lazy loading
const options = await interpretate(args[0], env);
const chart = new ApexCharts(env.element, options);
chart.render();
}
Our association will be in options
object after the interpretation.
Implement dynamic imports (lazy) if possible
Now we need to bundle this
npm run build
After the restart, it should work with our extension like a charm
However, after several evaluation dead instances of ApexCharts
will pile up as a garbage in Javascript memory. It is recommended to properly remove them. For that we need to identify each instance of ApexCharts
and assign destructor function
let ApexCharts;
core.ApexCharts = async (args, env) => {
if (!ApexCharts) ApexCharts = (await import('apexcharts')).default; //lazy loading
const options = await interpretate(args[0], env);
const chart = new ApexCharts(env.element, options);
chart.render();
env.local.chart = chart;
}
core.ApexCharts.destroy = (args, env) => {
env.local.chart.destroy();
}
core.ApexCharts.virtual = true
Now we need to bundle this
npm run build
After the restart it should work with our extension like a charm
Polishing
We might do a few more tweaks. If our ApexCharts
expression becomes too big, it might slow down the editor. For that reason we use Frontend Objects
BeginPackage["CoffeeLiqueur`Extensions`ApexCharts`", {
"CoffeeLiqueur`Extensions`Boxes`",
"CoffeeLiqueur`Extensions`FrontendObject`"
}]
(* Public context *)
ApexCharts /: MakeBoxes[a: ApexCharts[_Association], form: StandardForm ] := With[{o = CreateFrontEndObject[a]},
MakeBoxes[o, form]
] /; ByteCount[a] > 1024*4
Frontend objects has a special predefined StandardForm (and WLXForm), where it uses a reference to a Wolfram Expression stored separately.
Another improvement can be WLXForm, if we decide to use those charts on Slides
ApexCharts /: MakeBoxes[a: ApexCharts[_Association], form: WLXForm ] := With[{o = CreateFrontEndObject[a]},
MakeBoxes[o, form]
]
Full source code can be found in this repository